# -*- coding: utf-8 -*-
from jmespath import compile
from parsel import Selector
from parsel.utils import flatten

from .processors import Identity, MagicList, MagicStr, TakeFirst

__all__ = ['Compose', 'ItemLoader', 'JmesCompose', 'JmesLoader']


class Compose(object):
    """
    >>> sel = Selector(text='<a>data1</a><a>data2</a>')
    >>> Compose(sel.css('a').css(' ::text').getall()).proc(str.upper)
    DATA1
    >>> Compose(sel.css('a').css(' ::text').getall()).proc(str.upper, Join())
    DATA1
    >>> k3 = Compose(sel.css('a').css(' ::text').getall()).proc(str.upper, Join(), op=None)
    ['DATA1', 'DATA2']
    """

    def __init__(self, src_data):
        if src_data is None:
            self.src_data = []
        elif isinstance(src_data, (list, tuple)):
            self.src_data = src_data
        else:
            self.src_data = [src_data]

    @staticmethod
    def _save_mode(values):
        if isinstance(values, str):
            return MagicStr(values)
        elif isinstance(values, list):
            return MagicList(values)
        else:
            return values

    def _get_value(self, values, *processors, op=None):
        values = self._save_mode(values)
        for proc in processors:
            if values is None:
                break
            try:
                values = flatten(proc(value) for value in values if value)
            except Exception as e:
                break
        values = op(values)
        return self._save_mode(values)

    def proc(self, *processors, op=TakeFirst()):
        op = Identity() if op is None else op
        return self._get_value(self.src_data, *processors, op=op)


class ItemLoader(object):
    """
    >>> loader = ItemLoader(text='<a>data1</a><a>data2</a>')
    >>> mapping = {
    ...     'k1': ('css', 'a ::text'),
    ...     'k2': ('css', 'a ::text', [str.upper]),
    ...     'k3': ('css', 'a ::text', [str.upper], None),
    ...     'k4': ('css', 'a ::text', [str.upper], Join()),
    ... }
    >>> loader(mapping)
    {'k1': 'data1', 'k2': 'DATA1', 'k3': ['DATA1', 'DATA2'], 'k4': 'DATA1DATA2'}
    """

    def __init__(self, text, selector=Selector, **kwargs):
        self.selector = selector(text, **kwargs)

    def __call__(self, src_data: dict = None, **kwargs):
        if src_data is None:
            src_data = kwargs

        result = {}
        for key, value in src_data.items():
            assert 2 <= len(value) <= 4, "输入输出处理器可省略，示例：('css', '#demo', (ReFind(), Join()), TakeFirst())"

            if len(value) == 1:
                value = ('css', value, (), TakeFirst())
            elif len(value) == 2:
                value = (*value, (), TakeFirst())
            elif len(value) == 3:
                value = (*value, TakeFirst())

            assert value[0] in ['css', 'xpath'], 'css or xpath'

            processors = value[2]
            assert isinstance(processors, (tuple, list)), 'input processors must be tuple or list'

            data = getattr(self.selector, value[0])(value[1]).getall()
            result.update({key: Compose(data).proc(*processors, op=value[3])})

        return result


class JmesList(list):
    def __getitem__(self, pos):
        o = super().__getitem__(pos)
        return self.__class__(o) if isinstance(pos, slice) else o

    @staticmethod
    def _save_mode(values):
        if isinstance(values, str):
            return MagicStr(values)
        elif isinstance(values, list):
            return MagicList(values)
        else:
            return values

    def node(self, values):
        return self.__class__(flatten(flatten([x.node(values) for x in self])))

    def _get_value(self, values, *processors, op=None):
        values = self._save_mode(values)
        for proc in processors:
            if values is None:
                break
            try:
                values = flatten(proc(value) for value in values if value)
            except Exception as e:
                break
        values = op(values)
        return self._save_mode(values)

    def getall(self):
        return [x.getall() for x in self]

    def proc(self, *processors, op=TakeFirst()):
        op = Identity() if op is None else op
        return self._get_value(self.getall(), *processors, op=op)


class JmesCompose(object):
    """base on jmespath, http://jmespath.org/tutorial.html

    列表中有字符串一定要用单引号，比如：
        JmesCompose(result).node("data[?object.question.type=='question']").getall()

    >>> src_data = {
    ...     "people": [
    ...         {"first": "James", "last": "d"},
    ...         {"missing": "1111"},
    ...         {"first": "Jacob", "last": "e"},
    ...         {"first": "Jayden", "last": "f"},
    ...         {"missing": "different"}
    ...     ],
    ...     "foo": {"bar": "baz"},
    ...     'bar': 1,
    ... }
    >>> loader = JmesCompose(src_data)
    >>> loader.node('people[?keys(@)[?starts_with(@, `first`)]]')
    >>> for item in loader.node('people[?keys(@)[?starts_with(@, `first`)]]'):
            b = item.node('first').proc(lambda x: x.upper(), op=None)
            c = item.node('firsts').proc(lambda x: x.upper())         # default op=TakeFirst()
            d = item.node('last').proc()
            e = item.node('last').getall()
            print(b, c, d, e)
    ['JAMES'] None d ['d']
    ['JACOB'] None e ['e']
    ['JAYDEN'] None f ['f']

    >>> print(loader.node('people[][first, missing][]').proc(op=Join(' ')))
    James 1111 Jacob Jayden different
    """
    jme_cls = JmesList

    def __init__(self, src_data, _expr=None):
        self.src_data = src_data
        self._expr = _expr

    def _get_node(self, node):
        expr = compile(node)
        result = expr.search(self.src_data)
        return result

    def getall(self):
        return self.src_data

    def node(self, query):
        values = self._get_node(query)
        if values is None:
            values = []
        elif isinstance(values, (list, tuple)):
            pass
        else:
            values = [values]

        values = [self.__class__(v, query) for v in values]
        return self.jme_cls(values)

    def __bool__(self):
        return bool(self.getall())

    __nonzero__ = __bool__

    def __str__(self):
        data = repr(self.getall())
        return "<%s node=%r data=%s>" % (type(self).__name__, self._expr, data)

    __repr__ = __str__


class JmesLoader(object):
    """
    >>> loader = JmesLoader(src_data, parent='people[?keys(@)[?starts_with(@, `first`)]]')
    >>> loader(out_data={
    ...     'b': 'first',
    ...     'c': 'firsts',
    ...     'd': 'last',
    ... }, b=[(), Join(' ')])
    {'b': 'James Jacob Jayden', 'c': None, 'd': 'd'}

    >>> loader(out_data={
    ...     'b': ('first', (), Join(',')),
    ...     'c': 'firsts',
    ...     'd': 'last',
    ... })
    {'b': 'James,Jacob,Jayden', 'c': None, 'd': 'd'}
    """
    def __init__(self, src_data, parent=None, compose=JmesCompose, **kwargs):
        self.compose = compose(src_data, **kwargs)
        if parent is not None:
            self.compose = self.compose.node(parent)

    def __call__(self, out_data: dict, **kwargs):
        result = {}
        for key, value in out_data.items():
            if value is None:
                continue
            elif not isinstance(value, (list, tuple)):
                value = [value]

            assert 1 <= len(value) <= 3, "输入输出处理器可省略，示例：('people[][first, missing][]', (ReFind(), Join()), TakeFirst())"

            query = value[0]
            cover_proc = MagicList(kwargs.get(key, []))
            if len(value) == 1:
                value = (query, cover_proc[0] or (), cover_proc[1] or TakeFirst())
            elif len(value) == 2:
                value = (query, cover_proc[0] or value[1], cover_proc[1] or TakeFirst())

            processors = value[1]
            assert isinstance(processors, (tuple, list)), 'input processors must be tuple or list'

            result.update({key: self.compose.node(query).proc(*processors, op=value[2])})

        return result
